package com.avcompris.util.junit;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static org.junit.Assert.assertEquals;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Parameterized;
import org.junit.runners.Suite;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;

import com.avcompris.common.annotation.Nullable;

/**
 * This code has been copy/pasted from JUnit 4.7's
 * <tt>org.junit.runners.Parameterized</tt>.
 * 
 * @author David Andriana Copyright Avantage Compris SARL 2008-2009 ©
 */
public class AvcParameterized extends Suite {

	/**
	 * the test class to launch.
	 */
	private final TestClass testClass;

	/**
	 * inner runner.
	 */
	private static final class TestClassRunnerForParameters extends
			BlockJUnit4ClassRunner {

		/**
		 * the runner parameters.
		 */
		private final Object[] parameters;

		/**
		 * which parameter set is it.
		 */
		private final int parameterSetNumber;

		/**
		 * the constructor to use.
		 */
		private final Constructor<?> konstructor;

		/**
		 * the test class instance.
		 */
		private Object instance;

		/**
		 * constructor.
		 * 
		 * @param testClass
		 *            the test class to launch
		 * @param parameters
		 *            the test parameters
		 * @param i
		 *            which parameter set is it
		 */
		public TestClassRunnerForParameters(final TestClass testClass,
				@Nullable final Object[] parameters, final int i)
				throws InitializationError {

			super(nonNullArgument(testClass, "testClass").getJavaClass()); // todo

			this.parameters = parameters;
			parameterSetNumber = i;
			konstructor = getOnlyConstructor();
		}

		@Override
		protected Object createTest() throws Exception {

			return getInstance();
		}

		/**
		 * load the instance if needed.
		 * 
		 * @return the instance
		 * @throws InvocationTargetException
		 * @throws IllegalAccessException
		 * @throws InstantiationException
		 * @throws IllegalArgumentException
		 */

		private synchronized Object getInstance()
				throws InstantiationException, IllegalAccessException,
				InvocationTargetException {

			if (instance == null) {

				instance = konstructor.newInstance(parameters);
			}

			return instance;
		}

		@Override
		protected String getName() {

			final Object thisInstance;

			try {
				thisInstance = getInstance();

			} catch (final InstantiationException e) {
				throw new RuntimeException(e);
			} catch (final IllegalAccessException e) {
				throw new RuntimeException(e);
			} catch (final InvocationTargetException e) {
				throw new RuntimeException(e);
			}
			return String.format("[%s] %s", parameterSetNumber, thisInstance
					.toString());
		}

		@Override
		protected String testName(final FrameworkMethod method) {

			nonNullArgument(method, "method");

			return String.format("%s[%s] %s", method.getName(),
					parameterSetNumber, instance.toString());
		}

		/**
		 * assert there is only one constructor, and get it
		 * 
		 * @return the only constructor
		 */

		private Constructor<?> getOnlyConstructor() {
			final Constructor<?>[] constructors = getTestClass().getJavaClass()
					.getConstructors();
			assertEquals(1, constructors.length);
			return constructors[0];
		}

		@Override
		protected void validateConstructor(final List<Throwable> errors) {
			validateOnlyOneConstructor(errors);
		}

		@Override
		protected Statement classBlock(final RunNotifier notifier) {
			return childrenInvoker(notifier);
		}
	}

	/**
	 * runners.
	 */
	private final List<Runner> runners = new ArrayList<Runner>();

	/**
	 * constructor.
	 * 
	 * @param klass
	 *            the test class
	 */
	public AvcParameterized(final Class<?> klass) throws Throwable {

		super(nonNullArgument(klass, "class"), Collections.<Runner> emptyList());

		testClass = new TestClass(klass);

		// final MethodValidator methodValidator = new
		// MethodValidator(testClass);
		// methodValidator.validateStaticMethods();
		// methodValidator.validateInstanceMethods();
		// methodValidator.assertValid();

		int i = 0;
		for (final Object each : getParametersList()) {
			if (each instanceof Object[]) {

				runners.add(new TestClassRunnerForParameters(testClass,
						(Object[]) each, i));

				++i;

			} else {
				throw new Exception(String.format(
						"%s.%s() must return a Collection of arrays.",
						testClass.getName(), getParametersMethod().getName()));
			}
		}
	}

	@Override
	protected List<Runner> getChildren() {
		return runners;
	}

	/**
	 * get all the parameters as a list
	 * 
	 * @return the parameter list
	 */
	private Collection<?> getParametersList() throws Throwable,
			InvocationTargetException, Exception {
		return (Collection<?>) getParametersMethod().invokeExplosively(null);
	}

	/**
	 * get the static method annotated with {@link Parameterized.Parameters}.
	 * 
	 * @return the static annotated method
	 * @throws Exception
	 *             if no static method is found
	 */
	private FrameworkMethod getParametersMethod() throws Exception {
		final Collection<FrameworkMethod> methods = testClass
				.getAnnotatedMethods(Parameterized.Parameters.class);
		for (final FrameworkMethod each : methods) {
			final int modifiers = each.getMethod().getModifiers();
			if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) {
				return each;
			}
		}

		throw new Exception("No public static parameters method on class "
				+ getName());
	}

	/**
	 * transform an object array into a collection of object arrays, each final
	 * object array containing only one object.
	 * 
	 * @param params
	 *            the objects
	 * @return the resulting collection
	 */
	public static Collection<Object[]> eachOne(final Object... params) {

		nonNullArgument(params, "params");

		final Collection<Object[]> results = new ArrayList<Object[]>();
		for (final Object param : params) {
			results.add(new Object[] { param, });
		}
		return results;
	}
}
